v0.13.0
A new subcommand, declscope shrink, reports the exported declarations of internal/ packages that nothing outside their package uses. With -fix it unexports them.
Nothing the analyzer reports changes. Upgrading needs no edit.
declscope shrink
An exported declaration takes package 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. Unexported, it takes private, and the analyzer checks who reaches it.
The analyzer cannot find these on its own. It 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.
// 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:
$ declscope shrink
internal/user/user.go:8:6: func Format is exported, but nothing outside example.com/app/internal/user uses itdeclscope 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.
Important
Run shrink before the analyzer. A declaration it unexports becomes private to its namespace. Wherever another file of the package uses it, the analyzer then reports a crossing. Fixing in this order takes one pass each:
$ declscope shrink -fix ./...
$ declscope ./...Run them in the same order in CI. shrink exits 3 when it reports anything.
What it decides
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.
| Case | Result |
|---|---|
| Another package names it, writes it unkeyed, pairs its field in a conversion, or links it by name | Not reported |
| The compiler needs it to satisfy an interface | Not reported |
| Another module can reach it through a value an importable package hands out | Not reported |
| An API another package uses returns it, takes it, or holds it | Not reported |
A value of it escapes into an interface, where fmt, encoding/json or reflect can find it |
Not reported |
| Only an external test package uses it | Reported, with no fix |
A build-excluded file, a generated file, an example function or -ldflags -X may name it |
Reported, with no fix |
The new name would collide, be captured, or have no Go spelling (MAX_RETRIES) |
Reported, with no fix |
It does not judge a package outside internal/, package main, a package with assembly or cgo, or an internal/ a nested module may import. Each such internal/ package is named on stderr, with the reason.
Deleting unused code is out of scope. Once a declaration is unexported, staticcheck's unused and gopls' unusedfunc report it if nothing uses it.
A new rule: overexported
Only declscope shrink reports it. go vet and golangci-lint never do.
| Directive | Effect |
|---|---|
//declscope:ignore overexported |
Silences shrink for the declaration, its type's fields, or the file |
Bare //declscope:ignore |
Does not reach overexported. It still silences every rule the analyzer reports |
shrink reports an //declscope:ignore overexported that silenced nothing. The analyzer does not judge it, since it cannot see whether shrink needed it.
Other changes
| Change | Effect |
|---|---|
The adoption skill covers shrink |
declscope skill install now tells an agent what shrink is for, how to read a withheld fix, and to run it first |
| One comment binder | The analyzer and shrink find a declaration's directives through the same code, so both bind a directive alike |
| Lines as written | A directive binds by the line it sits on in the file, not the line a //line directive names |
Full changelog: v0.12.1...v0.13.0