Releases: mpyw/declscope
Release list
v0.14.0
Important
declscope v0.14.0 requires Go 1.27 and enables strict surplus and unused checks by default.
Strict checks
surplusreports each declaration that gets package scope from a directive but has no visible use from another namespace. Safe fixes add//declscope:private.unusedreports scope directives that do not change the scope in the current configuration, and ignores that silence no report. Safe fixes remove redundant scope directives.- Set either rule to
looseto judge a directive as a whole, oroffto disable the rule.
Go 1.27
Building and installing declscope requires Go 1.27 or newer.
Install
mise use "github:mpyw/declscope@0.14.0"Full changelog: v0.13.4...v0.14.0
v0.13.4
Changelog
- 3ce36f4 Bump version
v0.13.3
Changelog
v0.13.2
v0.13.1
Fixes to declscope shrink. Upgrading from v0.13.0 found each of these in a real repository. The analyzer is unchanged, and upgrading needs no edit.
Every fix below makes shrink report less, or offer fewer fixes. Where v0.13.0 offered a fix that broke a build, a go vet or a JSON output, v0.13.1 withholds it or does not report the declaration at all.
Fixes that no longer break anything
| Case | v0.13.0 | v0.13.1 |
|---|---|---|
A struct with a key:"value" field tag |
Unexported its fields, which go vet rejects for json and xml, and encoding/json drops silently |
The struct escapes, whole: a tag says a marshaller reads it. A tag on a struct it holds without naming counts too, an alias of one included |
| A type an exported API returns, takes or holds, where the API is used from another package | Unexported the type, leaving the API handing out a type its callers cannot name | Not reported: the other package holds values of it |
| The same, where the API is only used inside its package | As above | Reported, with no fix while the API keeps its name. The API and the type go together when both are fixed |
An alias an API returns (type QueryHandler = baseHandler[R]) |
Unexported the alias | Kept as the API's own name |
| A type embedded more than one level down | The new field name could make a selector ambiguous and break the build | Every type that holds it at any depth is checked |
An external test writing an embedded field by position (w.Outer{3}) |
Unexported the field's type, and the test stopped compiling | The literal is a use of the type |
One run of -fix converges
- Names are claimed only once it is settled which types keep their names.
- When two fixes lower to one name (
WrapandWRAP), only one of them is withheld. - A report without a fix reads the same after the fix as before it.
- When two fixes lower to one name (
- An
//declscope:ignore overexportedover a type that turns out used is reported unused.
Reasons for a package not judged
The reason printed on stderr for a package shrink does not judge now says which of these applies:
- the package is outside
internal/ - its
internal/parent lies above the module - a nested module may import it; the nested module is named, since one under
testdata/or_tools/is easy to miss
Full changelog: v0.13.0...v0.13.1
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
v0.12.1
v0.12.0 cannot be installed with the go command. This release fixes that and retracts v0.12.0. Its behaviour is the same as v0.12.0.
Important
Use v0.12.1 instead of v0.12.0. go install, go run and go get -tool of v0.12.0 fail:
go: github.com/mpyw/declscope/cmd/declscope@v0.12.0: create zip: testdata/src/symbolfile/★.go: malformed file path "testdata/src/symbolfile/★.go": invalid char '★'
The prebuilt binaries of v0.12.0 (mise, the release archives) work. For the changes in 0.12, see v0.12.0.
Fix
| v0.12.0 | v0.12.1 | |
|---|---|---|
go install ...@latest, go run ...@latest, go get -tool ...@latest |
Resolves to v0.12.0 and fails | Resolves to v0.12.1 and works |
go.mod |
retract v0.12.0, so @latest skips it |
|
| mise, release archives | Work | Work |
A test fixture had a file name that the module zip format refuses. That fixture is now written to a temporary directory by its test. A new test checks every file in the module against the module path rules, so such a name fails the tests before a release.
Migrating
| You pin | Change it to |
|---|---|
go install github.com/mpyw/declscope/cmd/declscope@v0.12.0 |
@v0.12.1 |
tool github.com/mpyw/declscope/cmd/declscope at v0.12.0 in go.mod |
go get -tool github.com/mpyw/declscope/cmd/declscope@v0.12.1 |
"github:mpyw/declscope" = "0.12.0" in mise.toml |
Optional: "0.12.1". The binaries behave the same |
Full changelog: v0.12.0...v0.12.1
v0.12.0
Unused directives get their own rule, unused, with three modes. rules.unused: strict also reports a scope directive that restates the scope a declaration already has under the current config, and -fix deletes it.
Important
This release changes which ignore silences an unused report. Unused reports now carry the unused rule. //declscope:ignore directive no longer silences them: write //declscope:ignore unused. A leftover ignore directive is itself reported, so a run finds each one. See Migrating.
Two rules: unused and directive
| Rule | Reports | Config |
|---|---|---|
unused |
A //declscope:ignore that silenced nothing, and a //declscope:package or //declscope:private that changes no scope |
rules.unused: off | loose | strict, default loose |
directive |
Malformed, unknown, conflicting and misplaced directives | No key, always on |
rules.unused: off | loose | strict
| Mode | //declscope:ignore is reported when |
//declscope:package or //declscope:private is reported when |
-fix |
|---|---|---|---|
off |
Never | Never | None |
loose (default) |
It silenced no report | Deleting it would change no declaration's scope under any config | None |
strict |
It silenced no report | Deleting it would change no declaration's scope under the current config | Deletes the directive, unless another report depends on it |
package app
type user struct {
//declscope:private
name string
}
//declscope:package
func Helper() {}
//declscope:ignore boundary
func greet(u user) string { return u.name }| Line | off |
loose |
strict |
|---|---|---|---|
| 4 | Nothing | Nothing | unused //declscope:private on user.name: it already has private scope |
| 8 | Nothing | unused //declscope:package on Helper: nothing it reaches takes a scope |
unused //declscope:package on Helper: it already has package scope |
| 11 | Nothing | unused //declscope:ignore boundary on greet |
unused //declscope:ignore boundary on greet |
Line 4 is private only because defaults.unexported is private. loose does not rely on the current config, so it keeps the directive. strict reports it.
loose stays the default, so no new report appears without a config edit. To opt in:
rules:
unused: strictWarning
Under strict, changing defaults.unexported changes the reports. Every directive that restates the new default is reported. One -fix run deletes them.
declscope survey names the mode among the rules in force, for example unused strict, and "unused": "strict" in the JSON rules object.
Which ignore silences an unused report
| The ignore | Silences an unused report for |
|---|---|
//declscope:ignore unused on the declaration |
The scope directive and the other ignores beside it |
Bare //declscope:ignore on the declaration |
The scope directive beside it |
//declscope:ignore unused, or a bare one, above the package clause |
Every directive in the file |
//declscope:ignore unused on a block or a type |
Nothing inside it |
No ignore silences its own unused report. This now holds for a file-level ignore too, which used to silence its own report.
Fixes
- Generic structs. An unkeyed literal such as
pair[string]{1, "x"}and a conversion from an instantiation now count as uses of the struct's fields.boundarymissed a private field used that way, andsurplus: strictcould suggest narrowing it. - False unused reports under
loose. A spec's directive is now judged against the block directive it overrides. A spec's//declscope:privateunder a//declscope:packageblock was reported as unused, although deleting it widens the spec. - Accurate block and file-level messages. An unused block or file-level directive names what is true: every spec states its own scope, the named specs already have that scope, or a nearer directive decides.
- Directive on
func _. It is reported asno checked declaration carries it, as onvar _, instead of misplaced. - Config errors. An unknown key under
filterlists the keys the section takes. A filter pattern that leaves its config's directory is refused asfilter "...", notexclude "...".
Migrating
| You see | Do this |
|---|---|
unused //declscope:ignore directive on X next to another unused report on X |
Change it to //declscope:ignore unused |
unused file-level //declscope:ignore ... |
Remove the ignore. It silenced nothing, including its own report |
Full changelog: v0.11.0...v0.12.0
v0.11.0
A directive is now exactly Go's directive form, //declscope:name, read by go/ast.ParseDirective. Every other spelling that is addressed to declscope is reported instead of being read.
Important
This release changes which comments are directives. A directive written with a space after //, a space after the colon, or as a block comment used to work. It is now reported and has no effect. See Migrating.
Only //declscope:name is a directive
A directive is a line comment with no space after //, no space after the colon, and a lowercase name. An argument follows after a space, and a trailing // reason is still allowed:
//declscope:package // shared with the reporting codeAny other comment whose text, after // or /*, starts with declscope: once any space is skipped is malformed. It is reported with one message, and it has no effect:
user.go:7:1: malformed declscope directive: write it as //declscope:name
| Form | Example | 0.10.1 | 0.11.0 |
|---|---|---|---|
Space or tab after // |
// declscope:package |
Read as a directive | Malformed |
| Space after the colon | //declscope: package |
Read as a directive | Malformed |
| Block comment | /*declscope:package*/ |
Read as a directive | Malformed |
| Uppercase name | //declscope:Package |
unknown directive |
Malformed |
| No name | //declscope: |
Ignored | Malformed |
This covers every keyword, at the declaration level, at the file level, and for a directive that binds to nothing. Prose that mentions a directive partway through a comment is not reported. An unknown lowercase name, such as //declscope:packagex, is still reported as unknown directive.
A malformed directive is reported rather than ignored. A directive that silently does nothing would widen or narrow a scope without anyone noticing.
Migrating
Run declscope 0.11.0 over the repository. Every comment to change is reported at its position.
| 0.10.1 | 0.11.0 |
|---|---|
// declscope:package |
//declscope:package |
//declscope: package |
//declscope:package |
/*declscope:package*/, /* declscope:package */ |
//declscope:package |
//declscope:Package |
//declscope:package |
//declscope: |
Remove it |
A file-level directive written as a block comment moves to a line comment above the package clause, as the other file-level directives are written.
Other changes
//go:linknameis now read throughgo/ast.ParseDirectivetoo, so a tab between it and its names is recognized. A declaration it names still gets no rename fix and nosurplusreport. cgo's//exportis read as before.- The README links the documentation site, which has a logo and a social preview (#107, #108).
Full changelog: v0.10.1...v0.11.0
v0.10.1
declscope baseline and declscope survey now analyze packages in parallel. Nothing else changes: the rules, the directives, the configuration, the baseline format, the flags and every output are what 0.10.0 gives.
Faster baseline and survey
Both commands already loaded and type-checked packages in parallel, then analyzed them one at a time. The analysis now runs on up to GOMAXPROCS goroutines, one package each.
| Target | 0.10.0 | 0.10.1 | Change |
|---|---|---|---|
Go standard library (381 packages), baseline |
2.89s | 2.20s | −24% |
Go standard library, survey |
2.60s | 2.27s | −13% |
A 94-package application, baseline and survey |
about 1.75s | about 1.7s | −1 to −2% |
Median of five runs on 12 cores. The gain depends on how much of a run is analysis rather than loading. Where loading dominates, as in most applications, there is little to gain.
The output is byte-identical to a sequential run. Each package is analyzed into its own slot, and the slots are combined in package order, so the order of entries and the first error reported do not depend on scheduling.
declscope ./... and go vet -vettool are unchanged. Their driver already analyzed packages in parallel. inspect analyzes one package, so it is unchanged too.
Full Changelog: v0.10.0...v0.10.1
- 19d1a20 baseline, survey: analyze packages in parallel