Skip to content

Releases: mpyw/declscope

v0.14.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 11:47
Immutable release. Only release title and notes can be modified.
02e42e0

Important

declscope v0.14.0 requires Go 1.27 and enables strict surplus and unused checks by default.

Strict checks

  • surplus reports each declaration that gets package scope from a directive but has no visible use from another namespace. Safe fixes add //declscope:private.
  • unused reports 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 loose to judge a directive as a whole, or off to 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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 07:39
Immutable release. Only release title and notes can be modified.
3ce36f4

Changelog

v0.13.3

Choose a tag to compare

@github-actions github-actions released this 25 Sep 07:37
Immutable release. Only release title and notes can be modified.
b439214

Changelog

v0.13.2

Choose a tag to compare

@github-actions github-actions released this 25 Sep 07:29
Immutable release. Only release title and notes can be modified.
1cc8d14

Changelog

  • 1cc8d14 feat: list the subcommands in the driver's help (#118)

v0.13.1

Choose a tag to compare

@github-actions github-actions released this 25 Sep 06:12
Immutable release. Only release title and notes can be modified.
56a6836

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 (Wrap and WRAP), only one of them is withheld.
    • A report without a fix reads the same after the fix as before it.
  • An //declscope:ignore overexported over 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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 04:10
Immutable release. Only release title and notes can be modified.
1843ee3

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 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.

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

Choose a tag to compare

@github-actions github-actions released this 24 Sep 07:25
Immutable release. Only release title and notes can be modified.
018ab11

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

v0.12.0 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 24 Sep 07:02
Immutable release. Only release title and notes can be modified.
846c990

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: strict

Warning

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. boundary missed a private field used that way, and surplus: strict could 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:private under a //declscope:package block 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 as no checked declaration carries it, as on var _, instead of misplaced.
  • Config errors. An unknown key under filter lists the keys the section takes. A filter pattern that leaves its config's directory is refused as filter "...", not exclude "...".

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

Choose a tag to compare

@github-actions github-actions released this 24 Sep 00:17
Immutable release. Only release title and notes can be modified.
6a65b83

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 code

Any 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:linkname is now read through go/ast.ParseDirective too, so a tab between it and its names is recognized. A declaration it names still gets no rename fix and no surplus report. cgo's //export is 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

Choose a tag to compare

@github-actions github-actions released this 22 Sep 23:48
Immutable release. Only release title and notes can be modified.
d5f1ac0

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