Skip to content

Directives

A directive is a line comment //declscope:name, with a lowercase name, no spaces, and any argument after a space. Any other comment starting with declscope: is malformed. A trailing // reason is ignored.

//declscope:shared // used by the reporting code
Directive Level Effect
//declscope:shared, //declscope:private Declaration or file States the scope. On a type it also reaches the type's fields, but not its methods
//declscope:ignore Declaration or file Silences every rule the analyzer reports. overexported must be named
//declscope:ignore <rules> Declaration or file Silences the named rules, comma-separated. Naming filter silences nothing
//declscope:core File Joins the file to the core namespace
//declscope:namespace <name> File Joins the file to an explicitly named namespace

Caution

//declscope:shared was spelled //declscope:package before v0.19.0, and defaults.unexported: shared was package. v0.20.0 and later read neither. To upgrade from v0.18.x or earlier, go through v0.19.0 first and run -fix there: see #186.

Placement

A declaration-level directive goes in the doc comment, directly above the declaration. A file-level directive goes above the package clause.

//declscope:shared

package database

//declscope:shared
func userSeed() {}

A directive on a block reaches every spec in it. A directive on one spec overrides the block's.

//declscope:shared
var (
    seed  = 1
    //declscope:private
    limit = 2 // this one is private
)

A directive on a type reaches its fields and its interface method names. It does not reach its methods, which take their own file's level.

Where a file-level directive may sit

//declscope:namespace must come before the package clause. Three placements are accepted there.

Placement Example
A blank line before package, as this README writes
//declscope:namespace billing

package app
Directly above package
//declscope:namespace billing
package app
At the bottom of the package doc comment, after a blank // line: Go's own convention for a directive in a doc comment
// Package app bills users.
//
//declscope:namespace billing
package app

Go excludes a //tool:name comment from a doc comment, so none of them reaches the rendered documentation.

Ignore levels

  • Every level is consulted. An ignore counts as used whenever it covers a rule that would have fired, so overlapping ignores never make one another look unused.
  • Ignores are consulted before the baseline. A suppression the baseline would also have absorbed still counts as used.

Unused directives

The unused rule reports a directive that changes nothing. A directive that declscope cannot read belongs to the directive rule instead.

Off, loose and strict

rules.unused //declscope:ignore is reported when //declscope:shared or //declscope:private is reported when -fix
off Never Never None
loose It silenced no report Deleting it would change no declaration's scope under any config None
strict (default) It silenced no report Deleting it would change no declaration's scope under the current config Deletes a redundant scope directive, unless withheld
  • One comment gets one report, however many declarations it reaches. A block's directive is reported once.
  • Without the directive, a declaration falls to the next row of scope resolution. Only defaults.unexported there depends on the config.
  • Under strict, a directive that loose keeps also stays when deleting it would make a nearer directive redundant.
  • off still leaves the directive rule on.
  • An ignore of boundary is not reported where a file the build leaves out names a declaration it reaches, or for a field, its type: a literal written by position names only the type. That file may cross into it under another build tag, where the ignore silences the crossing. The run that reads the file still reports an ignore with nothing to silence.

Warning

Under strict, changing defaults.unexported changes the reports. With unexported: shared, every //declscope:shared on an unexported declaration that nothing else widens is reported, and one -fix run deletes them. loose never reports a directive only because it names the current default.

Example: one file under each mode
package app

type user struct {
    //declscope:private
    name string
}

//declscope:shared
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:shared on Helper: nothing it reaches takes a scope unused //declscope:shared on Helper: it already has shared scope
11 Nothing unused //declscope:ignore boundary on greet unused //declscope:ignore boundary on greet

Line 4 is where the modes differ. Deleting line 4 leaves user.name private only because defaults.unexported is private, which loose does not rely on.

Report messages

Every unused report names the directive and says why it is unused. loose and strict word the reason differently.

The strict fix

-fix deletes a directive that strict reports, unless deleting it would change another report. Then the report stays, with no fix.

Example: one directive deleted, one withheld
// user.go
package app

//declscope:private
func normalize() {}

//declscope:private
func trim() {}
// order.go
package app

var _ = normalize
$ declscope ./...    # with rules.unused: strict
user.go:4:6: func normalize is declared private by //declscope:private, but is used from namespace "order"
order.go:3:9:   used here, in namespace "order"
user.go:3:1: unused //declscope:private on normalize: it already has private scope
user.go:6:1: unused //declscope:private on trim: it already has private scope

-fix changes user.go. The boundary report names normalize's directive, so that one stays.

 //declscope:private
 func normalize() {}

-//declscope:private
 func trim() {}

Silencing an unused report

Ignore Silences the unused report of
//declscope:ignore unused on a declaration Every other directive on that declaration
Bare //declscope:ignore on a declaration The scope directive on that declaration. Not the other ignores there, so a group of ignores cannot excuse one another
//declscope:ignore unused, or a bare one, above the package clause Every other directive in the file
Any ignore on a type or a block Nothing inside it. For other rules it reaches inside, but here one ignore would hide every stale directive under it

No ignore silences its own unused report.

Example
package app

//declscope:shared
//declscope:ignore unused
func A() {}

//declscope:shared
//declscope:ignore directive
func B() {}

//declscope:ignore unused
type T struct {
    //declscope:shared
    F int
}

//declscope:ignore qualify
//declscope:ignore unused
func C() {}

//declscope:ignore unused
func D() {}
$ declscope ./...
user.go:7:1: unused //declscope:shared on B: it already has shared scope
user.go:13:2: unused //declscope:shared on T.F: it already has shared scope
user.go:8:1: unused //declscope:ignore directive on B
user.go:11:1: unused //declscope:ignore unused on T
user.go:21:1: unused //declscope:ignore unused on D
Declaration Result Why
A Quiet ignore unused silences the scope directive's report
B Both reported ignore directive names another rule, so it silenced nothing
T Both reported The type's ignore does not reach the field
C Quiet ignore unused silences the report on ignore qualify
D Reported Its only candidate is its own report

Malformed directives

Only //declscope:name is a directive. Any other comment starting with declscope:, and an unknown, conflicting or misplaced directive, is reported by the directive rule and has no effect.

Every malformed-directive report

The rule is always on, and has no configuration key. //declscope:ignore directive silences it, on the declaration or above the package clause.

<name> stands for the directive's own name, such as shared or core.

Directive Report
//declscope:foo on a declaration unknown directive declscope:foo
//declscope:foo above the package clause declscope:foo is not a file-level directive
// declscope:shared, /*declscope:shared*/, or any other comment starting with declscope: that is not //declscope:name malformed declscope directive: write it as //declscope:name
//declscope:shared x, or an argument to another directive that takes none //declscope:<name> takes no argument
//declscope:namespace with no name declscope:namespace requires a name
//declscope:namespace Billing namespace "Billing" is not a valid lowerCamelCase identifier
//declscope:private and //declscope:shared together conflicting scope directives: ...
//declscope:core and //declscope:namespace together conflicting namespace directives: a core file's namespace is the core
//declscope:ignore foo unknown rule "foo" in declscope:ignore (want one of boundary, qualify, surplus, unused, directive, filter, overexported)
//declscope:ignore , or //declscope:ignore boundary, empty rule name in declscope:ignore
The same directive twice on one declaration or file. Two ignores naming the same rules, in any order, count as the same directive duplicate declscope:<name> directive, at the second
//declscope:namespace or //declscope:core after the package clause declscope:<name> must appear before the package clause
A valid shared, private or ignore directive attached to no declaration, such as one inside a function body misplaced declscope:<name>: no declaration here for it to bind to; ...