Rules¶
A rule is one check. A rule's name is the diagnostic's category, its baseline key, and what //declscope:ignore targets.
| Rule | Reports | Fix | Configured by | Default |
|---|---|---|---|---|
boundary |
A declaration used from outside the namespace it is private to | Insert //declscope:shared |
rules.boundary |
on |
qualify |
A name that does not carry its namespace | Rename to prefix it | rules.naming.* |
Off |
surplus |
Shared scope with no visible use from another namespace | Under strict, insert //declscope:private |
rules.surplus |
strict |
unused |
An ignore that silenced nothing, or a scope directive that changes no scope | Under strict, delete a redundant scope directive |
rules.unused |
strict |
directive |
A directive that is malformed, unknown, conflicting or misplaced | None | No | On |
filter |
A filter.only that an only above it cancels |
None | No | On |
overexported |
An exported declaration of an internal/ package that nothing outside its package uses |
Unexport it | Running declscope shrink |
Not run by the analyzer |
Every diagnostic carries at most one fix, so -fix never has to choose.
boundary¶
boundary reports a private declaration used from outside its namespace. For a member, the boundary is the namespace of its type. The example above is this rule.
The message names where the scope came from.
| Where the scope came from | Message |
|---|---|
defaults |
func normalizeEmail is private to namespace "userRepository", but is used from namespace "orderRepository" |
| The declaration's own directive | func normalizeEmail is declared private by //declscope:private, but is used from namespace "orderRepository" |
| Its type's directive | field Statement.wheres is declared private by //declscope:private on Statement, but is used from namespace "query" |
| A file-level directive | func normalizeEmail is declared private by the file's //declscope:private, but is used from namespace "orderRepository" |
Every crossing use site is attached to the diagnostic, and a use from inside the declaration's own namespace is not. The report lands on the declaration, not on the use.
| Case | Fix |
|---|---|
| The declaration states its own scope | None. The directive and the use are both deliberate, and -fix must not overwrite what the author wrote |
The private is inherited from a type or a file |
Offered. The inserted directive sits on the declaration, which outranks both |
| A type and its members cross together | One directive, on the type. The members are reported without a fix, since a second directive on them would bind nothing |
Reach without a boundary¶
rules.boundary: off switches this rule off. What is left is the naming rule, for a repository that wants the ownership mark in a name without the scope behind it.
Set surplus: off alongside it. surplus audits //declscope:shared, which means nothing once reach is not checked.
Tip
This is not how to adopt declscope gradually. A baseline records what a codebase already has and still reports what is new. A switch reports nothing, so you never learn what turning it on later would cost.
The naming rule¶
Off by default. Turn it on with rules.naming.qualify.
The rule asks a package-level declaration to carry its file's namespace somewhere in its name. The name then says which unit owns it, at the call site and in a stack trace.
// order_repository.go
func scanOrder(rows *sql.Rows) (domain.Order, error) {
// ...
o.BuyerEmail = normalizeEmail(email) // the email unit's, and shared
}
The mark grants nothing. Reach is stated by scope alone.
Note
The rule is opt-in because whether a prefix reads well depends on the file name, and no tool can see that. A prefix from comments.go reads as a noun phrase, commentsAttached. A prefix from collect.go reads as a command, collectAddFunc.
When it applies¶
rules.naming.qualify |
Effect |
|---|---|
never (default) |
Off |
ondemand |
Required once the package has a second namespace. A file holding only a package clause and comments, as a doc.go usually does, is not counted |
always |
Required in every package, so that gaining a second namespace is not a mass rename |
Declarations the rule never reaches
Members and the core namespace are explained in their own sections.
| Declaration | Example | Reason |
|---|---|---|
| A member, or a method written beside its type | Already qualified by that type at every use | |
func main in package main |
A name the toolchain requires | |
A test function in a _test.go file |
The same. go test finds TestXxx, BenchmarkXxx, FuzzXxx and ExampleXxx by name |
|
| A declaration in a namespace that cannot start an identifier | The fix prefixes, and no identifier begins with a digit. A namespace whose first letter has no capital form, such as 用户 or _foo, is skipped too: an exported name could not take it and stay exported |
|
An exported identifier, unless rules.naming.exported is on |
How the API is spelled is the author's decision | |
| A declaration in the core namespace | The core has no prefix |
What carries a namespace¶
The namespace must appear in the name, ignoring case, starting at a word boundary. The match may end inside a word.
In user_id.go, namespace userID |
Carries it |
|---|---|
userIDCache, userIdCache |
Yes |
loadUserID, parseUserIds |
Yes. Anywhere in the name, and the right edge may run on |
poweruserID |
No. user does not start a word there |
user in user.go |
Yes. The name is the namespace |
The left edge is anchored because the right one is not. Without the anchor, key would be found in monkey.
Two English inflections change the namespace's own spelling. Both are accepted.
| Namespace | Also carried by |
|---|---|
store |
storing. The final e drops before ing |
apply |
applies, applied. The final y turns to i |
Only these whole forms are generated, from the namespace's side. The name is never stemmed, so story and storm do not carry store.
The reverse is not generated: in tracing.go, traceValue is reported. The message names the vocabulary key to add.
rules.naming.vocabulary lists extra words that carry a namespace. A listed word goes through the same test, so wheelDelta carries mouse and pinwheel does not.
Warning
The vocabulary is for the irregular few. A namespace that needs a long list is a sign that the file declares things it is not about. Splitting the file says more than listing them.
The rename¶
The fix prefixes, keeping Go's spelling of an initialism and the original exportedness. When the name opens with the namespace's last words, only the missing words are prefixed. When that name is taken, the fix prefixes the whole namespace.
| Case | Example | Never |
|---|---|---|
| Unexported | id → userID |
userId |
Exported, under rules.naming.exported |
Load → UserLoad |
userLoad |
| The name opens with the namespace's last words | FunctionCallExpr in ast_function_call_expr.go → AstFunctionCallExpr |
AstFunctionCallExprFunctionCallExpr |
The suggestion is one answer, not the only one. Any spelling that carries the namespace settles the rule. A rename never changes what a name is visible to.
Note
Sometimes the namespace is what is wrong, not the name. A file name may hold words that name no unit.
user_repository.go:18:6: func scanUser does not carry namespace "userRepository" anywhere in its name; rename it to userRepositoryScanUser, or to another name that carries "userRepository"
The unit here is user, not userRepository. //declscope:namespace user on the file settles the rule and leaves every name alone.
Withheld renames¶
A rename is offered only when it provably changes nothing but the spelling. The violation is reported either way. A doubt withholds the fix, never the diagnostic.
Caution
Go resolves a name from the inside out, so a new name that is free at package level can still be bound at a use site. The wrong rename compiles and computes something else.
A method, or a type that is embedded, is renamed only when the new name is free on every type that holds it. A method is also not renamed when an interface of the package asks for its old or new name, since a type assertion would change its answer.
A package that holds a non-Go source file, such as assembly (.s) for any architecture, gets no rename fix. Assembly names Go functions by text, which the rename cannot follow, and the build would fail at link time. Rename by hand, or add //declscope:ignore qualify.
surplus¶
surplus is the converse of boundary. It reports shared scope that no visible use from another namespace needs.
rules.surplus |
Reports | Fix |
|---|---|---|
off |
Nothing | |
loose |
A //declscope:shared that nothing it reaches needs |
None |
strict (default) |
What loose reports, plus each declaration a directive in use widens for nothing |
Insert //declscope:private |
$ declscope ./...
email.go:3:1: //declscope:shared on normalizeEmail: no use from another namespace is visible to declscope
One comment gets one report, however many declarations take their scope from it. A declaration that states its own scope does not depend on the comment, so it neither keeps it alive nor appears under it.
Important
The rule concludes from an absence. A directive can hold up something declscope cannot see, so the rule stays quiet on any doubt. For the same reason loose offers no fix: its advice is to delete a directive.
strict¶
Under loose, one used declaration keeps its whole directive quiet. The rest of what the directive reaches may still be wider than it needs. strict reports each of those, and -fix narrows it.
// account.go
package bank
//declscope:shared
type account struct {
id int
balance int
}
func accountDeposit(a *account, n int) { a.balance += n }
$ declscope ./...
account.go:6:2: field account.balance takes shared scope from //declscope:shared on account, but no use from another namespace is visible to declscope
-fix changes account.go:
Every enclosing directive is judged the same way.
| The directive is on | Judged one by one |
|---|---|
| A struct or interface type | Each field, or each method name |
A var, const or type block |
Each spec |
| The file | Each declaration in the file, and each member of a type that states no scope |
strict is the default. loose checks only directives as a whole; select it explicitly if that is the policy your repository wants.
Tip
Convention puts private fields last, after the fields other namespaces read. The fix never reorders fields: order is observable through unkeyed composite literals, positional encodings, unsafe offsets and 64-bit atomic alignment. Move them yourself where none of those apply.