go-ruleguard dsl

The Go API for authoring ruleguard rule files that match, filter, and report on Go source code.

Library
Go
vv0.3.23
879stars
BSD 3-Clause License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
46/100Fair
Development Activity0
Maintenance32
Community52
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
78/100Good
Architecture82
Code Quality78
Innovation80
Learning Curve70

dsl is the package that ruleguard rule files import to define custom static-analysis rules in plain, tooling-friendly Go syntax. Rule authors write ordinary .go files that declare matcher functions accepting a dsl.Matcher argument, then chain Match()/MatchComment(), Where(), Report(), and Suggest() calls to describe what code pattern to look for, what additional type/value constraints must hold, and what warning or quickfix to emit.

Critically, none of the functions in this package actually execute at runtime for the caller — Match, Where, Report, and friends are effectively documented no-ops. The real ruleguard analyzer (in the parent go-ruleguard module) parses rule files as an AST and interprets the call chains directly, which is what lets rule files be checked by go vet, formatted by gofmt, and navigated by any Go-aware editor while still being pure configuration rather than compiled code.

Because it’s just a type-and-signature surface with no runtime behavior of its own, the dsl package is extremely stable and lightweight to depend on — projects add it purely so their rules.go files type-check, then ship the rules to be executed by the separate ruleguard binary or through golangci-lint’s gocritic/ruleguard integration.

What You Get

  • Matcher.Match/MatchComment to declare one or more AST or comment-regexp patterns a rule should trigger on
  • Matcher.Where to attach additional boolean constraints (purity, constness, type identity, and more) that gate whether a match is accepted
  • Matcher.Report/Suggest to emit a diagnostic message or a quickfix rewrite pattern when a rule matches
  • A rich Var API exposing captured submatch metadata — type, AST node kind, computed value, addressability, comparability — for use inside Where conditions
  • Matcher.Import/ImportAs to register package import paths so qualified type names resolve correctly during rule compilation
  • ImportRules/Bundle to compose and share rule groups as importable Go packages

Common Use Cases

  • Writing a project-specific rules.go file with gorules rules to catch team-specific anti-patterns that generic linters miss
  • Building and publishing a distributable ruleguard rule bundle as its own Go module for other teams to import
  • Wiring custom ruleguard rules into golangci-lint’s gocritic integration for use in CI
  • Prototyping and debugging a single lint rule interactively via ruleguard -e before adding it to a permanent rule set
  • Embedding ruleguard-style dynamic rule execution into a custom static analyzer, following go-critic’s pattern

Under The Hood

Architecture The dsl package is a pure type-and-signature surface: every exported method (Matcher.Match, Where, Report, Var.Filter, ExprType.Is, and so on) returns a static zero value and has no real logic in its body — the actual rule engine lives in the sibling analyzer/ruleguard packages of the parent go-ruleguard module (not in dsl itself), which parse a rules.go file’s AST, recognize these exact method-chain shapes, and compile them into an internal matcher representation executed against gogrep-derived AST patterns. This split means a rule file that imports dsl type-checks and gofmt-formats like normal Go code while never actually being built or run as Go code — the dsl package exists purely to give the rule-authoring surface real Go types, so editors, go vet, and IDE autocomplete all work against genuine method signatures instead of a bespoke DSL parser.

Tech Stack dsl is dependency-free Go (module github.com/quasilyte/go-ruleguard/dsl, declared at the conservative go 1.15 in its own go.mod even though the parent repo has moved to go 1.22 with a toolchain pin), split into a handful of small files — dsl.go for the Matcher/Var pattern-matching API, filter.go and types subpackage for the VarFilterContext/types.Type filter surface used inside Var.Filter callbacks, do.go for the DoContext/DoVar rewrite-action API, and bundle.go for cross-package rule composition via ImportRules. The parent repository consuming this package builds the real analyzer on top of golang.org/x/tools’s go/analysis framework, quasilyte/gogrep for AST pattern matching, and go-toolsmith helpers.

Code Quality There are no dedicated test files inside the dsl package itself — appropriate given its methods are declared zero-value stubs by design, with actual behavior verified by the parent repository’s analyzer/ruleguard test suites that exercise real rule files end-to-end. Naming is disciplined and consistently documented: every exported type and method carries a doc comment explaining semantics and, in several cases, worked examples (e.g. MatchComment’s named-capture-group walkthrough). The parent monorepo enforces gofmt, goimports, govet, staticcheck, gosimple, and several other golangci-lint linters in CI across the whole module, including this package.

API Design The API reads as a fluent, chainable builder — m.Match(...).Where(...).Report(...) — that mirrors how a rule is described in prose, which keeps rule files readable even as constraints stack up. Method names map directly to Go analysis vocabulary (Deadcode, Addressable, Comparable, AssignableTo, Implements, OfKind) so anyone familiar with go/types semantics can guess correctly, and boilerplate to get started is minimal — a valid rule file needs only a gorules package clause, a dsl import, and one matcher function. The tradeoff is that the API’s true behavior is invisible from the package alone: understanding what a call chain actually does requires knowing it’s interpreted by an external AST-walking compiler rather than executed as written, which is undocumented within the package itself and only becomes clear from the parent project’s README.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search