A Go linter that checks package imports are in a list of acceptable packages. This allows you to allow imports from a whole organization or only allow specific packages within a repository.
go install github.com/OpenPeeDeeP/depguard/v2/cmd/depguard@latestThe Depguard binary looks for a file named ^\.?depguard\.(yaml|yml|json|toml)$ in the current working directory. Examples include (.depguard.yml or depguard.toml).
The following is an example configuration file.
{
"main": {
"files": [
"$all",
"!$test"
],
"listMode": "Strict",
"allow": [
"$gostd",
"github.com/OpenPeeDeeP"
],
"deny": {
"reflect": "Who needs reflection",
}
},
"tests": {
"files": [
"$test"
],
"listMode": "Lax",
"deny": {
"github.com/stretchr/testify": "Please use standard library for tests"
}
}
}- The top level is a map of lists. The key of the map is a name that shows up in the linter's output.
files- list of file globs that will match this list of settings to compare againstallow- list of allowed packagesdeny- map of packages that are not allowed where the value is a suggestionlistMode- the mode to use for package matching
Files are matched using Globs. If the files
list is empty, then all files will match that list. Prefixing a file
with an exclamation mark ! will put that glob in a "don't match" list. A file
will match a list if it is allowed and not denied.
Should always prefix a file glob with
**/as files are matched against absolute paths.
** matches any number of characters, including /, but it doesn't make the
slashes around it optional. So **/internal/**/*.go matches
internal/foo/a.go but not internal/a.go: there has to be at least one folder
between internal/ and the file. To match zero or more folders, put the slash
inside a brace group with an empty option: **/internal/{**/,}*.go.
| Glob | /project/internal/a.go |
/project/internal/foo/a.go |
|---|---|---|
**/internal/**/*.go |
no | yes |
**/internal/{**/,}*.go |
yes | yes |
**/internal/{**,}/*.go |
no | yes |
The last one doesn't work because the empty option leaves internal//*.go.
Allow is a list of packages to allow. Deny is a map where the key is a package to deny, and the value is a suggestion on what to use instead (it can be empty).
ListMode is used to determine the package matching priority. There are three different modes; Original, Strict, and Lax.
Original is the original way that the package was written to use. It is not recommended to stay with this and is only here for backwards compatibility.
Strict, at its roots, is everything is denied unless in allowed.
Lax, at its roots, is everything is allowed unless it is denied.
Every allow and deny entry is a prefix: it matches an import if the
import path starts with it. This is a plain string comparison, it does not look
at path segments. End an entry with a dollar sign $ to match that exact
package only. Matching is case-sensitive, like Go import paths, so use the
package's exact casing (github.com/OpenPeeDeeP/, not github.com/openpeedeep/).
| Entry | Matches | Does not match |
|---|---|---|
github.com/bob |
github.com/bob, github.com/bob/pkg, github.com/bob2/pkg |
|
github.com/bob/ |
github.com/bob/pkg |
github.com/bob, github.com/bob2/pkg |
github.com/bob$ |
github.com/bob |
github.com/bob/pkg, github.com/bob2/pkg |
Some things to keep in mind:
- To match a package and all of its subpackages, but nothing else, use two
entries:
github.com/bob$andgithub.com/bob/. - A new major version shares the old version's prefix.
math/randalso matchesmath/rand/v2, so denymath/rand$to only deny the old version. - Entries without a dot or slash are prefixes too:
osmatchesos/exec, but alsoosextandos.example.com/pkg. For the standard library, prefer$gostd(which lists each package exactly) oros$andos/.
Within a list, the longest matching entry is used. With this deny list:
deny:
github.com/org/legacy/: "use github.com/org/v2"
github.com/org/legacy/auth/: "use github.com/org/authn"github.com/org/legacy/auth/token gets the authn suggestion, and every other
package under github.com/org/legacy/ gets the v2 suggestion.
When an import matches both the allow and deny lists, the list mode decides:
- Original: deny always wins. An empty allow list allows everything.
- Strict: the import must match an allow entry. If it also matches a deny entry, it is only allowed when the allow entry is longer (more specific) than the deny entry.
- Lax: the import is allowed unless it matches a deny entry. If it also matches an allow entry that is longer than the deny entry, it is allowed.
An exact entry counts as one character longer than the same entry without the
$, so allow: [foo$] beats deny: {foo: ...} for the import foo itself, but
foo/bar is still denied. Entries of the same length are denied.
For example, in Strict mode with allow: [github.com/org/] and
deny: {github.com/org/legacy/: ...}, github.com/org/api is allowed and
github.com/org/legacy/db is denied. The settings_test.go file has many more
scenarios under TestListImportAllowed.
Each list a file matches is checked on its own, and an import must be allowed
by all of them. Every list that denies it reports its own error. Put the
rules every file shares in one list (for example with $all), and add separate
lists for the extra rules of a subset of files (for example $test).
There are variable replacements for each type of list (file or package). This is to reduce repetition and tedious behaviors.
you can still use an exclamation mark
!in front of a variable to say not to use it. Example!$testwill match any file that is not a go test file.
$all- matches all go files$test- matches all go test files
$gostd- matches all of go's standard library packages exactly (each one as if it ended with$). If theGOROOTenvironment variable is set, packages are read from$GOROOT/src, otherwise they come fromgo list stdusing the go toolchain for the current directory. Internal, vendored, andcmdpackages are not included.
An entry you write yourself takes priority over the same entry from a variable. With
deny: {$gostd: "no std", net/http$: "use our client"}, net/http gets the
use our client suggestion and every other standard library package gets no std.
Below:
- non-test go files will match
Mainand test go files will matchTest. - both allow all of go standard library except for the
reflectpackage which will tell the user "Please don't use reflect package". - go test files are also allowed to use https://github.com/stretchr/testify package and any sub-package of it.
Main:
files:
- $all
- "!$test"
allow:
- $gostd
deny:
reflect: Please don't use reflect package
Test:
files:
- $test
allow:
- $gostd
- github.com/stretchr/testify
deny:
reflect: Please don't use reflect packageBelow:
- All go files will match
Main - Go files in internal will match both
MainandInternal
Main:
files:
- $all
Internal:
files:
- "**/internal/{**/,}*.go"Below:
- All packages are allowed except for
github.com/OpenPeeDeeP/depguard. Thoughgithub.com/OpenPeeDeeP/depguard/v2andgithub.com/OpenPeeDeeP/depguard/somepackagewould be allowed.
Main:
deny:
github.com/OpenPeeDeeP/depguard$: Please use v2This linter was built with golangci-lint in mind, read the linters docs to see how to configure all their linters, including this one.
The config is similar to the YAML depguard config documented above, however due to golangci-lint limitation the deny value must be provided as a list, with pkg and desc keys (otherwise a panic may occur):
# golangci-lint config
linters-settings:
depguard:
rules:
prevent_unmaintained_packages:
list-mode: lax # allow unless explicitely denied
files:
- $all
- "!$test"
allow:
- $gostd
deny:
- pkg: io/ioutil
desc: "replaced by io and os packages since Go 1.16: https://tip.golang.org/doc/go1.16#ioutil"AI tools (Claude) are used in this project as a discussion partner for reviewing changes, weighing design options, and talking through ideas. The AI does not write the linter's code; that is written by the maintainer and contributors. It may assist with some unit tests and documentation, but most of those are also written by hand, and anything it contributes is reviewed by the maintainer before it is committed.