-
Notifications
You must be signed in to change notification settings - Fork 0
lanekeep checks .go files. Rules are TypeScript programs matching a Go syntax tree — see
Home for why.
go get -tool github.com/fmsouza/lanekeep/cmd/lanekeep
go tool lanekeep check ./...That pins lanekeep in go.mod beside every other tool. Go's tooling installs and pins only
things written in Go, so that package is a small launcher: it fetches the real binary on first
use, verifies it against the release's published checksums, and caches it by version.
LANEKEEP_BINARY=/usr/local/bin/lanekeep go tool lanekeep check ./...skips the fetch entirely — the answer for air-gapped CI. So does having lanekeep installed by any other route; the launcher checks its cache before reaching out.
brew install fmsouza/tap/lanekeep and the releases page
work too.
lanekeep init in a directory with a go.mod writes this for you:
{
"$schema": "https://raw.githubusercontent.com/fmsouza/lanekeep/main/schema/lanekeep.schema.json",
"include": ["**/*.go"],
"exclude": ["**/*_test.go"],
"rules": [
"lanekeep/no-package-init",
"./lanekeep/rules/no-fmt-println.ts"
]
}Generated code carries the same .go extension as everything else, so exclude it by path:
{ "exclude": ["**/*_test.go", "**/*.pb.go", "**/mock_*.go"] }// bad
type Client struct { ctx context.Context }
// good
func (c *Client) Do(ctx context.Context) error { return nil }A stored context outlives the call it was scoped to, so cancellation stops meaning what the
caller intended — a long-lived client holds the context of whichever request happened to build
it, and cancelling that request cancels unrelated work. Both context.Context and
*context.Context are reported.
Known false positive: a package aliased to context and exposing a Context is reported
like the standard library's. bindingKind says a name is an import, not which module.
// bad
func init() { registry["pg"] = newPostgres() }
// good
func Register(r map[string]Driver) { r["pg"] = newPostgres() }init runs at import time in an order the language decides. Nothing calls it, so nothing says
when it happens — and two packages registering into a shared map depend on an order neither
states. Every init in a file is reported; Go permits several, which is what makes the
ordering hard to reason about. A method named init is not reported: it is called
explicitly.
import { defineRule } from 'lanekeep'
export default defineRule({
id: 'local/no-naked-return',
language: 'go', // ← required; the default is TypeScript
severity: 'error',
card: {
message: 'naked return in a function with named results',
remediation: 'return the values explicitly, so the reader does not have to scroll for them',
examples: {
bad: 'func f() (err error) {\n\treturn\n}',
good: 'func f() (err error) {\n\treturn err\n}',
},
},
query: '(function_declaration result: (parameter_list) body: (block)) @fn',
check(ctx, m) {
for (const ret of ctx.querySubtree(m.fn, '(return_statement) @r')) {
if (ctx.namedChildren(ret.r).length === 0) ctx.report(ret.r)
}
},
})language: 'go' is not optional. The default is ['typescript', 'tsx'], and a rule does
not run on a file whose language it does not name — so omitting it means the rule silently
never fires.
ctx.bindingKind understands:
| Form | Kind |
|---|---|
import "net/http" → http
|
import |
var x, x := 1, const c
|
var, var, const
|
type T struct{}, type T = U
|
type |
func (r *Repo) M() → r
|
receiver |
func F[T any]() → T
|
type-param |
| parameters and named results | param |
for i := range xs |
loop |
Two Go-specific behaviors worth knowing:
Package-level declarations are order-independent. A function may call one declared two hundred lines below it, and resolution reflects that.
Several statements are scopes without being blocks. if x := f(); x != nil binds x
across the condition, the consequence and the else branch. switch t := v.(type) binds a
differently-typed t in each case clause.
The blank identifier never resolves. _ discards rather than binds.
Import paths versus names. import "net/http" binds http, while the module recorded is
net/http. A rule matching the identifier sees http; one matching the module needs the full
path.
Verified against the grammar, and each cost a round of failing tests when building Go support:
- A three-clause
fornests its parts in afor_clause; the initializer is not a field offor_statement. -
nameis a repeated field.var a, b inthas two, and reading only the first losesb. - A
blockholds its statements one level down, in astatement_list. -
type_speccarriestype_parametersfor a generic type declaration.
Languages
In the repository